refactor(test/cli): migrate harness short-lived path to AppProcess + FileSystem#28366
Merged
Conversation
…rvices Replace the tmpdir + short-lived spawn machinery in withCliFixture: - `os.tmpdir + fs.mkdir + addFinalizer` → `FileSystem.makeTempDirectoryScoped`. Cleanup is now scope-driven without a hand-rolled finalizer. - `Process.run` (legacy cross-spawn wrapper) → `AppProcess.run`, which is the same service already used by `git`, `ripgrep`, `format`, `mcp`, etc. One spawn primitive across src + tests. `stdin: "ignore"` is set explicitly because `ChildProcess.make` defaults stdin to "pipe", which caused `opencode run` to block on `Bun.stdin.text()` waiting for an EOF that the test never sent. The old wrapper defaulted to ignore, so this preserves the prior contract. `AppFileSystem.defaultLayer` + `AppProcess.defaultLayer` are provided internally so callers' signatures don't change. The help-snapshot path regex widens from `[a-z0-9]+` to `[A-Za-z0-9]+` because Node's `mkdtemp` suffix is mixed-case (unlike the prior `Math.random().toString(36)` suffix). Slice 1 of 2. Slice 2 will migrate the long-lived `serve` / `acp` builders, which use raw `Bun.spawn` today.
Two simplify-pass refinements on top of the harness migration:
1. Pass `timeout` into `AppProcess.run` instead of wrapping with an
external `Effect.timeoutOrElse`. AppProcess.run is itself scoped, so
the built-in timeout triggers the acquireRelease kill finalizer
*before* surfacing the error — guaranteeing the child is dead by the
time the test continues. The external wrapper variant raced the
scope close and could leak the child past the test boundary.
2. Replace `Effect.orDie` with `Effect.catchTag("AppProcessError", ...)`
so timeouts AND spawn failures both turn into a synthetic non-zero
result with the error's command / stderr / cause as diagnostics —
instead of crashing as a defect. The synthetic result is `satisfies
AppProcess.RunResult` so it stays in sync with process.ts.
3. Name the spawn Effect span `"opencode.spawn"` via `Effect.fn` to
match the existing `Effect.fn("opencode.serve")` / `.acp` siblings.
3 tasks
AIALRA-0
pushed a commit
to AIALRA-0/opencode-turn-engine
that referenced
this pull request
Jun 10, 2026
AIALRA-0
pushed a commit
to AIALRA-0/opencode-turn-engine
that referenced
this pull request
Jun 10, 2026
avion23
pushed a commit
to avion23/opencode
that referenced
this pull request
Jun 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Slice 1 of 2 migrating the CLI subprocess test harness from raw
Bun.spawn/cross-spawnwrappers to the Effect-native services already used elsewhere in the codebase.This slice covers the two lower-risk surfaces:
Tmpdir —
os.tmpdir + fs.mkdir + addFinalizer→FileSystem.makeTempDirectoryScoped({ prefix: "oc-cli-" }). Cleanup is now scope-driven without a hand-rolled finalizer.Short-lived
spawn—Process.run(legacy cross-spawn wrapper fromsrc/util/process.ts) →AppProcess.run, which is the same service already used bygit,ripgrep,format,mcp, etc. Single spawn primitive across src + tests.The long-lived
serve/acpbuilders still use rawBun.spawn— those land in slice 2.Notes
stdin: "ignore"set explicitly.ChildProcess.makedefaults to"pipe", which madeopencode runblock onBun.stdin.text()waiting for EOF that never came. OldProcess.rundefaulted to ignore — this preserves the prior contract.AppProcessError; converted viatimeoutOrElseto a synthetic non-zero result soexpectExitstill drives the failure path.AppFileSystem.defaultLayer+AppProcess.defaultLayerprovided insidewithCliFixtureso external call sites +CliFixtureshape are unchanged.[A-Za-z0-9]+— Node'smkdtempsuffix is mixed-case, unlike the priorMath.random().toString(36).Test plan
bun run typecheckcleanbun run test test/cli/— 333 pass, 0 fail (was 333 pass with this diff applied; the 4 prior timeouts were the stdin issue caught + fixed before merge)